Skip to content

StaticFetcher() Implementation + from_pdb() refactor - #5436

Open
jauy123 wants to merge 110 commits into
MDAnalysis:developfrom
jauy123:Fetcher_Class
Open

StaticFetcher() Implementation + from_pdb() refactor#5436
jauy123 wants to merge 110 commits into
MDAnalysis:developfrom
jauy123:Fetcher_Class

Conversation

@jauy123

@jauy123 jauy123 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #5429 and #5431

Changes made in this Pull Request:

  • Implements fetch.fetchers.StaticFetcher()
  • Refactors fetch.pdb.from_pdb() to use StaticFetchers()

LLM / AI generated code disclosure

LLMs or other AI-powered tools (beyond simple IDE use cases) were used in this contribution: yes / no
From comment: #5436 (comment)

Per AI policy, I'm declaring that I used in AI in primary two ways: IDE autocomplete and help with reviewing the docstring used for the documentation to make it look more professional.

The one notable exception is that I used AI to help me draft test_multiple_downloads_existing_database and test_existing_database in testsuite/MDAnalysisTests/fetch/test_static_fetcher.py

I prompt is "Is there a unique way to determine the hash in order if two files are exactly the same including modification time in python using the standard library"

It gave code that used os.stat(), so I googled it and found pathlib.Path.stat().st_mtime in the official [docs](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat) which I then used manually for test_existing_database and test_multiple_downloads_existing_database with no AI.

PR Checklist

  • Issue raised/referenced?
  • Tests updated/added?
  • Documentation updated/added?
  • package/CHANGELOG file updated?
  • Is your name in package/AUTHORS? (If it is not, add it!)
  • LLM/AI disclosure was updated.

Developers Certificate of Origin

I certify that I can submit this code contribution as described in the Developer Certificate of Origin, under the MDAnalysis LICENSE.

@jauy123

jauy123 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Docs need to be reupdated, but the code itself feel like it is in a completed state.

@jauy123
jauy123 requested a review from yuxuanzhuang August 4, 2026 21:44
@read-the-docs-community

read-the-docs-community Bot commented Aug 5, 2026

Copy link
Copy Markdown

@jauy123 jauy123 mentioned this pull request Aug 9, 2026
4 tasks

@yuxuanzhuang yuxuanzhuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the code is in pretty good shape. I will add more comments related to the code.

For the documentation, it would be helpful to add an overview page at package/doc/sphinx/source/documentation_pages/fetchers_modules.rst that includes:

  1. Usage examples.
  2. A list of the available fetchers.

See package/doc/sphinx/source/documentation_pages/converters.rst for an example.


cache_files = [
path
for path in self.cache_path.rglob("*")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking whether a requested file is present in the registry should not require scanning the entire cache directory. That is error-prone since unrelated cache files can also cause false failures; it also adds unnecessary I/O.

@jauy123 jauy123 Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check_registry() might be a misleading name. This method is meant to return all files not in the self.cache_path per the docstring

Return paths relative to :attr:`cache_path` for cache files that are
missing from the registry.

I originally made it when I was refactoring the fetch() method where it would toss error to prevent unintentional update of the registry (which would be a security issue)

        if LOAD_FROM_CACHE:
            registry_dictionary = self.read_registry(db_path)
            missing_files_list = self.check_registry(
                db_path, files=list(requested_files)
            )

            if len(missing_files_list) != 0:
                MISSING_FILES = True

        if MISSING_FILES and not APPEND_DATABASE:
            raise ValueError(
                "fetch() is requesting files not found in the registry. "
                + f"The missing files are {missing_files_list}. "
                + "To fix this, please set append_db=True to append the "
                + "registry."
            )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method could be renamed because it's not really checking like return a True or False

Comment thread .gitignore Outdated
Comment thread package/MDAnalysis/fetch/fetchers.py Outdated
Comment thread package/MDAnalysis/fetch/fetchers.py
Comment thread package/MDAnalysis/fetch/fetchers.py Outdated
Comment thread package/MDAnalysis/fetch/fetchers.py Outdated
Comment thread package/MDAnalysis/fetch/fetchers.py Outdated
jauy123 and others added 4 commits August 11, 2026 16:11
Co-authored-by: Yuxuan Zhuang <yuzhuang@stanford.edu>
Co-authored-by: Yuxuan Zhuang <yuzhuang@stanford.edu>
Co-authored-by: Yuxuan Zhuang <yuzhuang@stanford.edu>
Co-authored-by: Yuxuan Zhuang <yuzhuang@stanford.edu>
@jauy123 jauy123 mentioned this pull request Aug 12, 2026
7 tasks

@orbeckst orbeckst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see my comments on StaticFetcher in #5457 (review) and apply them here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added comments to PR #5457 as I hadn't realized that you're first introducin StaticFetcher here. Please see #5457 (review)

@BradyAJohnston BradyAJohnston left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some mostly minor things around the place, but biggest thing is the return type.

I think it should be single item in -> single item out, sequence in -> sequence of Path out rather than always returning a list[Path].

Path(
main_downloader.fetch(
fname=file_name,
progressbar=verbose,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verbose is currently not passed along properly. Pooch only reads the progressbar argument of Pooch.fetch() when no downloader is given (if downloader is None: downloader = choose_downloader(url, progressbar=progressbar)), and StaticFetcher always supplies one.

from_PDB(progressbar=True) only works because the unknown progressbar kwarg leaks through **kwargs into HTTPDownloader(progressbar=...).

Explicitly forward progressbar=verbose into the downloader constructor inside _set_downloader, having from_PDB call verbose=progressbar, and adding a test that asserts the downloader receives it.

if APPEND_DATABASE and LOAD_FROM_CACHE:
self.append_registry(db_path, requested_files)

return paths[0] if len(paths) == 1 else paths

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does change the current return type compared to develop which always returned a list of Path items.

I think we should have single item / string in -> Path out, sequence in -> list[Path] out, otherwise people will constantly have to unpack the first item in the list to use it.

registry_dictionary.setdefault(name, None)

# Download code using pooch
main_downloader = pooch.create(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A scenario worth thinking through: the registry stores the hash of whatever was first downloaded. If the upstream file later changes (PDB entries do get revised) and the cached copy has been evicted, pooch raises a hash mismatch. Before that, retry_if_failed retries the download twice with sleeps, which can never fix a hash mismatch. The only recovery is deleting hashes.txt, and pooch's error message doesn't say so.

Related: for from_PDB, which always passes append_db=True, the registry offers no protection against a bad server because the hash is derived from the download itself. It only detects local cache corruption. That's fine, but the docs should say so.

Suggestions: catch pooch's hash ValueError and re-raise with a recovery hint, and decide explicitly whether from_PDB needs a registry at all.

for line in f:

# Pooch registry files can contain comments
if line.lstrip()[0] == "#":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring promises the pooch registry format, but two valid pooch registry files fail here:

  • A blank line (e.g. trailing newline after a hand edit) raises IndexError on line.lstrip()[0].
  • The optional third URL column (fname hash url) raises ValueError: too many values to unpack on the next line.

Skip empty lines (if not line.strip() or line.lstrip().startswith("#")) and take only the first two fields, or delegate to pooch's own load_registry.

return [
path
for path in cache_files
if (path.name not in database_files) and (path not in ignore)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignore doesn't behave as documented. The docstring and example say entries are relative to cache_path (ignore=["file2.txt"]), but this compares against absolute Path objects from rglob, so a relative entry never matches. I confirmed ignore=["b.txt"] leaves b.txt in the result while ignore=[cache_path / "b.txt"] removes it. The test only passes absolute paths, which is why it passes.

Resolve ignore entries against cache_path the same way files is handled a few lines up.

append_db=False,
downloader="auto",
**kwargs,
):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return and argument type hinting


return paths[0] if len(paths) == 1 else paths

def append_registry(self, db_path, files, write_duplicate=False):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type hinting


self.write_registry(Path(db_path), _new_files, mode="a")

def check_registry(self, db_path, files=None, ignore=None):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type hinting

if (path.name not in database_files) and (path not in ignore)
]

def read_registry(self, db_path):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type hitnig


return hash_dict

def write_registry(self, db_path, files, mode="w"):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type hinting

@jauy123

jauy123 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Ok, I just saw the comments today. I will spend time this weekend addressing them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fetcher GSoC GSoC project

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[GSoC 2026] Implementation of Static Fetcher

6 participants